Pythonのテストコードでmockを使ってみた
こんにちは、みかみです。
そろそろ梅の良い季節v(今週末は、湯島天神梅まつり?!
はじめに
やりたいこと
- Web APIからレスポンスを取得する Python コードのテストをしたい
- HTTP接続できない環境でもテストできるようにしたい
- Python で mock を使ってみたい!
動作環境
- Windows10(Mac VMware Fusion)
- Python 3.6.0(unittest を pip install 済み)
やってみた
Web APIからレスポンスを取得する関数のテスト
お天気APIで、東京のお天気を取得するコードです。
Python の HTTPライブラリ requests でGETリクエストを投げてレスポンスを取得しています。
import requests def get_resp(url): resp = requests.get(url) if resp.status_code != 200: print('Get Data Failed...(error code : {})'.format(resp.status_code)) raise return resp.json() def main(): url = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=130010' actual = get_resp(url) print(actual) if __name__ == '__main__': main()
実行すると、APIの response を print します。
C:\Users\mikami.yuki\work\apiTest>py get_weather.py {'pinpointLocations': [{'link': 'http://weather.livedoor.com/area/forecast/1310100', 'name': '千代田区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310200', 'name': '中央区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310300', 'name': '港区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310400', 'name': '新宿区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310500', 'name': '文京区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310600', 'name': '台東区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310700', 'name': '墨田区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310800', 'name': '江東区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310900', 'name': '品川区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311000', 'name': '目黒区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311100', 'name': '大田区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311200', 'name': '世田谷区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311300', 'name': '渋谷区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311400', 'name': '中野区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311500', 'name': '杉並区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311600', 'name': '豊島区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311700', 'name': '北区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311800', 'name': '荒川区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311900', 'name': '板橋区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312000', 'name': '練馬区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312100', 'name': '足立区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312200', 'name': '葛飾区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312300', 'name': '江戸川区'}, {'link': 'http://weather.livedoor.com/area/forecast/1320100', 'name': '八王子市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320200', 'name': '立川市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320300', 'name': '武蔵野市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320400', 'name': '三鷹市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320500', 'name': ' 青梅市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320600', 'name': '府中市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320700', 'name': '昭島市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320800', 'name': '調布市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320900', 'name': '町田市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321000', 'name': '小金井市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321100', 'name': '小平市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321200', 'name': '日 野市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321300', 'name': '東村山市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321400', 'name': '国分寺市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321500', 'name': '国立市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321800', 'name': '福生市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321900', 'name': '狛江市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322000', 'name': '東大和市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322100', 'name': ' 清瀬市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322200', 'name': '東久留米市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322300', 'name': '武蔵村山市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322400', 'name': '多摩市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322500', 'name': '稲城市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322700', 'name': '羽村市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322800', 'name': 'あきる野市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322900', 'name': '西東京市'}, {'link': 'http://weather.livedoor.com/area/forecast/1330300', 'name': '瑞穂町'}, {'link': 'http://weather.livedoor.com/area/forecast/1330500', 'name': '日の出町'}, {'link': 'http://weather.livedoor.com/area/forecast/1330700', 'name': '檜原村'}, {'link': 'http://weather.livedoor.com/area/forecast/1330800', 'name': '奥多摩町'}], 'link': 'http://weather.livedoor.com/area/forecast/130010', 'forecasts': [{'dateLabel': '今日', 'telop': '晴のち曇', 'date': '2017-02-08', 'temperature': {'min': None, 'max': None}, 'image': {'width': 50, 'url': 'http://weather.livedoor.com/img/icon/5.gif', 'title': '晴のち曇', 'height': 31}}, {'dateLabel': '明日', 'telop': '曇時々雨', 'date': '2017-02-09', 'temperature': {'min': {'celsius': '2', 'fahrenheit': '35.6'}, 'max': {'celsius': '5', 'fahrenheit': '41.0'}}, 'image': {'width': 50, 'url': 'http://weather.livedoor.com/img/icon/10.gif', 'title': '曇時々雨', 'height': 31}}, {'dateLabel': '明後日', 'telop': '曇時々晴', 'date': '2017-02-10', 'temperature': {'min': None, 'max': None}, 'image': {'width': 50, 'url': 'http://weather.livedoor.com/img/icon/9.gif', 'title': '曇時々晴', 'height': 31}}], 'location': {'city': '東京', 'area': '関東', 'prefecture': '東京都'}, 'publicTime': '2017-02-08T17:00:00+0900', 'copyright': {'provider': [{'link': 'http://tenki.jp/', 'name': '日本気象協会'}], 'link': 'http://weather.livedoor.com/', 'title': '(C) LINE Corporation', 'image': {'width': 118, 'link': 'http://weather.livedoor.com/', 'url': 'http://weather.livedoor.com/img/cmn/livedoor.gif', 'title': 'livedoor 天気情 報', 'height': 26}}, 'title': '東京都 東京 の天気', 'description': {'text': ' 日本付近は、冬型の気圧配置が緩んできました。\n\n【関東甲信地方】\n 関東甲信地方は、曇りとなっています。\n\n 8日は、気圧の谷の影響で曇りとなり、雨や雪の降る所があるでしょう。\n\n 9日は、低気圧が本州南岸を東に 進むため、曇りで時々雨や雪が降るでし\nょう。伊豆諸島では、雷を伴う所がある見込みです。\n\n 関東近海では、8日はうねりを伴い波が高いでしょう。9日はしける所が\nある見込みです。船舶は高波に注意してください。\n\n【東京地方】\n 8日は、曇りとなるでしょう。\n 9日は、曇り時々雨か雪となる見込み です。', 'publicTime': '2017-02-08T22:02:00+0900'}}
この requests lib をmock化したい!
テストコードを書きます。
from get_weather import get_resp import unittest from unittest import mock # requests lib の mock def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code def json(self): return self.json_data if args[0] == 'url_valid': return MockResponse({"key1": "value1"}, 200) return MockResponse({}, 404) # テスト用クラス class TestGetResp(unittest.TestCase): # 正常系確認テストケース @mock.patch('requests.get', side_effect=mocked_requests_get) def test_get_resp_ok(self, mock_get): json_data = get_resp('url_valid') self.assertEqual(json_data, {"key1": "value1"}) # 異常系確認テストケース @mock.patch('requests.get', side_effect=mocked_requests_get) def test_get_resp_ng(self, mock_get): with self.assertRaises(RuntimeError): json_data = get_resp('url_invalid') if __name__ == '__main__': unittest.main()
実行してみます。
C:\Users\mikami.yuki\work\apiTest>py test_get_weather.py Get Data Failed...(error code : 404) .. ---------------------------------------------------------------------- Ran 2 tests in 0.002s OK
requests.get() をmock化できました!
mockをもっとシンプルに使ってみる
例えば、
- main() から hoge() をコール
- hoge() が fuga() をコールして、fuga() の戻り値を main() に返す
- main() で戻り値をprint
なコードがあるとします↓
def hoge(): return fuga() def fuga(): return 'Here is Fuga!' def main(): res = hoge() print(res) if __name__ == '__main__': main()
実行してみると
C:\Users\mikami.yuki\work\apiTest>py test_mock.py Here is Fuga!
ちゃんと、fuga() の戻り値をprintしてくれます。
この hoge() のユニットテストをしたいので、テストコードから fuga() をコールします。
が、 fuga() とは分離したいので、fuga() を mock に置き換えてみます↓
from unittest import mock def hoge(): return fuga() def fuga(): return 'Here is Fuga!' # fuga() を patch(mock化) @mock.patch('__main__.fuga') def test_hoge(mock_fuga): # mock の戻り値を設定 mock_fuga.return_value = 'Here is Mock!' res = hoge() print(res) if __name__ == '__main__': test_hoge()
たったこれだけ!
実行してみます。
C:\Users\mikami.yuki\work\apiTest>py test_mock.py Here is Mock!
ちゃんと mock が呼ばれました!
おわりに(所感)
なに、これ、すごい!(めっさらくちん@@v
- Java → Junit → Eclipse で Plugin がどうとか、アノテーションがどうとか。。(あせ
- PHP@Laravel + Mockery → UnitTestのこと考えて初めからファサードクラス実装したり、composer で Mockery 入れたり、めんどく(ry
→ Python の unittestってすぐれものv
※個人的感想すみませんmm(近頃 Python 信者なもので。。